// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © BOSWaves

//@version=6
indicator("Liquidity Trap Detector [BOSWaves]", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ┌────────────────────────────── BOSWaves ─ Constants ──────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
const string g1 = "Trend Engine"
const string g2 = "Level Detection"
const string g3 = "Trap Logic"
const string g4 = "Zone Construction"
const string g5 = "Visualization"

color BULL = #00FF00
color BEAR = #FF0066

// ┌────────────────────────────── BOSWaves ─ Inputs ─────────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘

// ─────────────────── Trend Engine ───────────────────
int trendLen      = input.int(26, "DEMA Length", minval = 5, group = g1,
     tooltip = "Number of bars used to calculate the DEMA trend baseline. Higher values produce a smoother, slower baseline.")
int trendSmooth   = input.int(2, "DEMA Smoothing", minval = 1, maxval = 10, group = g1,
     tooltip = "Additional EMA smoothing applied to the DEMA baseline. A value of 1 disables the extra smoothing.")
int trendAtrLen   = input.int(78, "ATR Length", minval = 5, group = g1,
     tooltip = "Number of bars used to calculate the Average True Range for the trend flip thresholds and gradient width.")
float trendBand   = input.float(2.55, "Flip Threshold ATR", minval = 0.25, maxval = 3.0, step = 0.05, group = g1,
     tooltip = "Distance from the DEMA baseline, as a multiple of ATR, that price must close beyond to flip the trend. The baseline must also be sloping in the new direction. Higher values require stronger displacement.")
float cloudWidth  = input.float(0.8, "Gradient Width ATR", minval = 0.20, maxval = 2.0, step = 0.05, group = g1,
     tooltip = "Width of the trend gradient drawn from the DEMA baseline, as a multiple of ATR.")

// ─────────────────── Level Detection ───────────────────
int swingLen      = input.int(5, "Swing Length", minval = 2, maxval = 30, group = g2,
     tooltip = "Number of bars on each side required to confirm a swing high or low used as a breakout level.")
int maxLevels     = input.int(6, "Levels Per Side", minval = 1, maxval = 20, group = g2,
     tooltip = "Maximum number of swing levels tracked on each side. The oldest level is discarded when a new one forms.")
int maxAge        = input.int(300, "Level Max Age (bars)", minval = 20, maxval = 2000, group = g2,
     tooltip = "Maximum number of bars an unbroken swing level is tracked after it forms before it is discarded.")

// ─────────────────── Trap Logic ───────────────────
int atrLen        = input.int(14, "ATR Length", minval = 1, group = g3,
     tooltip = "Number of bars used to calculate the Average True Range for the breakout buffer.")
float breakBuf    = input.float(0.1, "Break Buffer (ATR)", minval = 0.0, step = 0.05, group = g3,
     tooltip = "Distance beyond the level, as a multiple of ATR, that price must close to register a breakout. Higher values require a more decisive break.")
int trapWindow    = input.int(5, "Trap Window (bars)", minval = 1, maxval = 30, group = g3,
     tooltip = "Maximum number of bars after a breakout for price to close back through the level. If it does, a trap is confirmed. If it doesn't, the breakout is accepted and the level is discarded.")

// ─────────────────── Zone Construction ───────────────────
int maxZones      = input.int(4, "Zones Per Side", minval = 1, maxval = 10, group = g4,
     tooltip = "Maximum number of active trap zones displayed on each side. The oldest zone is removed when a new one forms.")
string mitigation = input.string("Absolute", "Mitigation", options = ["Absolute", "Middle"], group = g4,
     tooltip = "Defines when a trap zone is invalidated and removed. Absolute requires a close beyond the trap extreme. Middle requires a close beyond the zone midpoint.")
bool hideOverlap  = input.bool(true, "Hide Overlap", group = g4,
     tooltip = "When enabled, a new trap zone replaces an existing zone on the same side that it overlaps.")
int extendBars    = input.int(40, "Zone Extension", minval = 10, maxval = 500, step = 10, group = g4,
     tooltip = "Number of bars the trap zones and level lines are projected forward from the current bar.")

// ─────────────────── Visualization ───────────────────
bool showTrend    = input.bool(true, "Show Trend Gradient", group = g5,
     tooltip = "Toggles visibility of the DEMA trend gradient.")
bool paintBars    = input.bool(true, "Color Candles", group = g5,
     tooltip = "Toggles coloring of candles based on the current trend direction.")
bool showSignals  = input.bool(true, "Show Trend Flips", group = g5,
     tooltip = "Toggles triangle markers on bullish and bearish trend flips.")
bool showLabel    = input.bool(true, "Show Trap Text", group = g5,
     tooltip = "Toggles the BULL TRAP and BEAR TRAP text inside each zone.")
bool showMetric   = input.bool(true, "Show Trapped Volume", group = g5,
     tooltip = "Toggles the trapped volume reading beneath the trap text. Requires Show Trap Text to be enabled and volume data on the instrument.")
color bullCol     = input.color(BULL, "Bear Trap / Bull Trend", inline = "col", group = g5,
     tooltip = "Color used for bear trap zones, bullish trend elements, and bullish retest markers.")
color bearCol     = input.color(BEAR, "Bull Trap / Bear Trend", inline = "col", group = g5,
     tooltip = "Color used for bull trap zones, bearish trend elements, and bearish retest markers.")
int gradRows      = input.int(4, "Trap Gradient Layers", minval = 2, maxval = 6, group = g5,
     tooltip = "Number of gradient layers inside each trap zone. Layers are most opaque at the broken level and fade toward the trap extreme.")

// ┌────────────────────────────── BOSWaves ─ DEMA Trend ─────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
float src = hlc3
float ema1 = ta.ema(src, trendLen)
float ema2 = ta.ema(ema1, trendLen)
float demaRaw = 2.0 * ema1 - ema2
float basis = trendSmooth > 1 ? ta.ema(demaRaw, trendSmooth) : demaRaw
float tAtr = ta.atr(trendAtrLen)

float upperFlip = basis + tAtr * trendBand
float lowerFlip = basis - tAtr * trendBand

var int trend = 0

if trend == 0
    trend := close >= basis ? 1 : -1
else if trend == -1 and close > upperFlip and basis > basis[1]
    trend := 1
else if trend == 1 and close < lowerFlip and basis < basis[1]
    trend := -1

bool switchUp = trend == 1 and trend[1] == -1
bool switchDown = trend == -1 and trend[1] == 1
color trendCol = trend == 1 ? bullCol : bearCol

// ┌────────────────────────────── BOSWaves ─ Trend Gradient ─────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
float cloud = tAtr * cloudWidth

float c0 = basis
float c1 = trend == 1 ? basis - cloud * 0.20 : basis + cloud * 0.20
float c2 = trend == 1 ? basis - cloud * 0.40 : basis + cloud * 0.40
float c3 = trend == 1 ? basis - cloud * 0.60 : basis + cloud * 0.60
float c4 = trend == 1 ? basis - cloud * 0.80 : basis + cloud * 0.80
float c5 = trend == 1 ? basis - cloud : basis + cloud

p0 = plot(showTrend ? c0 : na, color=color.new(trendCol, 100))
p1 = plot(showTrend ? c1 : na, color=color.new(trendCol, 100))
p2 = plot(showTrend ? c2 : na, color=color.new(trendCol, 100))
p3 = plot(showTrend ? c3 : na, color=color.new(trendCol, 100))
p4 = plot(showTrend ? c4 : na, color=color.new(trendCol, 100))
p5 = plot(showTrend ? c5 : na, color=color.new(trendCol, 100))

fill(p0, p1, color=color.new(trendCol, 22))
fill(p1, p2, color=color.new(trendCol, 32))
fill(p2, p3, color=color.new(trendCol, 44))
fill(p3, p4, color=color.new(trendCol, 57))
fill(p4, p5, color=color.new(trendCol, 72))

barcolor(paintBars ? trendCol : na)

plotshape(showSignals and switchUp, title="Bull Trend", style=shape.triangleup, location=location.belowbar, color=bullCol, size=size.tiny)
plotshape(showSignals and switchDown, title="Bear Trend", style=shape.triangledown, location=location.abovebar, color=bearCol, size=size.tiny)

// ┌────────────────────────────── BOSWaves ─ Types ──────────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
type Level
    float price
    int pivBi
    bool isHigh
    bool broken
    int breakBi
    int breakT
    float extreme
    float trapVol

type TrapZone
    float top
    float btm
    float trig
    float total_vol
    int left_time
    bool bearish
    bool armed

type TrapDraw
    box ob
    box eOB
    array<box> gradBoxes
    line trigLn
    line trigExt
    line farLn
    line farExt

// ┌────────────────────────────── BOSWaves ─ Variables ──────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
var Level[] highLevels = array.new<Level>()
var Level[] lowLevels = array.new<Level>()

var TrapZone[] bullTraps = array.new<TrapZone>()
var TrapZone[] bearTraps = array.new<TrapZone>()

var TrapDraw[] bullDraws = array.new<TrapDraw>()
var TrapDraw[] bearDraws = array.new<TrapDraw>()

float atr = ta.atr(atrLen)
float ph = ta.pivothigh(high, swingLen, swingLen)
float pl = ta.pivotlow(low, swingLen, swingLen)

// ┌────────────────────────────── BOSWaves ─ Helpers ────────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
f_vol_text(float v) =>
    v >= 1000000000 ? str.tostring(math.round(v / 1000000000, 2)) + "B" :
     v >= 1000000 ? str.tostring(math.round(v / 1000000, 2)) + "M" :
     v >= 1000 ? str.tostring(math.round(v / 1000, 2)) + "K" :
     str.tostring(math.round(v))

f_delete_draw(TrapDraw d) =>
    box.delete(d.ob)
    box.delete(d.eOB)

    for b in d.gradBoxes
        b.delete()

    d.gradBoxes.clear()

    line.delete(d.trigLn)
    line.delete(d.trigExt)
    line.delete(d.farLn)
    line.delete(d.farExt)

f_create_draw(color css) =>
    box _ob = box.new(na, na, na, na, xloc=xloc.bar_time, border_color=color.new(css, 70), bgcolor=color.new(color.black, 100), border_width=1)
    box _eOB = box.new(na, na, na, na, xloc=xloc.bar_time, border_color=color.new(css, 80), bgcolor=color.new(color.black, 100), border_width=1)

    gradB = array.new<box>()

    for i = 0 to gradRows - 1
        gradB.push(box.new(na, na, na, na, xloc=xloc.bar_time, border_color=color.new(color.black, 100), bgcolor=na, border_width=0))

    line tL = line.new(na, na, na, na, xloc=xloc.bar_time, color=na, width=2)
    line tE = line.new(na, na, na, na, xloc=xloc.bar_time, color=na, width=2)
    line fL = line.new(na, na, na, na, xloc=xloc.bar_time, color=na, width=1)
    line fE = line.new(na, na, na, na, xloc=xloc.bar_time, color=na, width=1)

    TrapDraw.new(_ob, _eOB, gradB, tL, tE, fL, fE)

f_add(TrapZone[] zArr, TrapDraw[] dArr, TrapZone z, color css) =>
    if hideOverlap and zArr.size() > 0
        for k = zArr.size() - 1 to 0
            TrapZone ez = zArr.get(k)

            if z.btm < ez.top and z.top > ez.btm
                f_delete_draw(dArr.get(k))
                zArr.remove(k)
                dArr.remove(k)
                break

    zArr.unshift(z)
    dArr.unshift(f_create_draw(css))

    while zArr.size() > maxZones
        zArr.pop()
        f_delete_draw(dArr.pop())

f_push_trap(Level lv) =>
    bool bearish = lv.isHigh
    float top = bearish ? lv.extreme : lv.price
    float btm = bearish ? lv.price : lv.extreme
    TrapZone z = TrapZone.new(top, btm, lv.price, lv.trapVol, lv.breakT, bearish, false)

    if bearish
        f_add(bullTraps, bullDraws, z, bearCol)
    else
        f_add(bearTraps, bearDraws, z, bullCol)

// ┌────────────────────────────── BOSWaves ─ Level Tracking ─────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
f_new_level(Level[] arr, float price, bool isHigh) =>
    arr.unshift(Level.new(price, bar_index - swingLen, isHigh, false, na, na, na, 0.0))

    if arr.size() > maxLevels
        arr.pop()

f_process_levels(Level[] arr) =>
    bool fired = false

    if arr.size() > 0
        for i = arr.size() - 1 to 0
            Level lv = arr.get(i)
            bool kill = false

            if not lv.broken
                bool brk = lv.isHigh ? close > lv.price + atr * breakBuf : close < lv.price - atr * breakBuf

                if brk
                    lv.broken := true
                    lv.breakBi := bar_index
                    lv.breakT := time
                    lv.extreme := lv.isHigh ? high : low
                    lv.trapVol := nz(volume)

                else if bar_index - lv.pivBi > maxAge
                    kill := true

            else
                lv.extreme := lv.isHigh ? math.max(lv.extreme, high) : math.min(lv.extreme, low)
                lv.trapVol += nz(volume)

                bool back = lv.isHigh ? close < lv.price : close > lv.price

                if back
                    f_push_trap(lv)
                    fired := true
                    kill := true

                else if bar_index - lv.breakBi >= trapWindow
                    kill := true

            if kill
                arr.remove(i)

    fired

if not na(ph)
    f_new_level(highLevels, ph, true)

if not na(pl)
    f_new_level(lowLevels, pl, false)

bool bullTrap = f_process_levels(highLevels)
bool bearTrap = f_process_levels(lowLevels)

// ┌────────────────────────────── BOSWaves ─ Mitigation & Retests ───────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
f_process_zones(TrapZone[] zArr, TrapDraw[] dArr) =>
    bool retest = false

    if zArr.size() > 0
        for k = zArr.size() - 1 to 0
            TrapZone z = zArr.get(k)
            float mid = math.avg(z.top, z.btm)
            float tg = mitigation == "Middle" ? mid : z.bearish ? z.top : z.btm
            bool invalid = z.bearish ? close > tg : close < tg

            if invalid
                f_delete_draw(dArr.get(k))
                zArr.remove(k)
                dArr.remove(k)

            else
                bool outside = z.bearish ? high < z.btm : low > z.top
                bool touch = z.bearish ? high >= z.btm : low <= z.top

                if outside
                    z.armed := true

                else if touch and z.armed
                    z.armed := false
                    retest := true

    retest

bool bullTrapRetest = f_process_zones(bullTraps, bullDraws)
bool bearTrapRetest = f_process_zones(bearTraps, bearDraws)

// ┌────────────────────────────── BOSWaves ─ Draw Zones ─────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
f_draw_all(TrapZone[] zArr, TrapDraw[] dArr, color css) =>
    if barstate.islast and zArr.size() > 0
        int dt = time - time[1]
        int extTime = time + dt * extendBars

        for i = 0 to zArr.size() - 1
            TrapZone z = zArr.get(i)
            TrapDraw d = dArr.get(i)
            float rowH = (z.top - z.btm) / gradRows
            float far = z.bearish ? z.top : z.btm

            // Gradient layers
            for g = 0 to d.gradBoxes.size() - 1
                int transp = int(78 + float(g) / float(gradRows) * 17)
                float rTop = z.bearish ? z.btm + (g + 1) * rowH : z.top - g * rowH
                float rBot = rTop - rowH

                d.gradBoxes.get(g).set_lefttop(z.left_time, rTop)
                d.gradBoxes.get(g).set_rightbottom(extTime, rBot)
                d.gradBoxes.get(g).set_bgcolor(color.new(css, transp))

            // Outer shell
            d.ob.set_lefttop(z.left_time, z.top)
            d.ob.set_rightbottom(time, z.btm)

            // Extended box
            d.eOB.set_lefttop(time, z.top)
            d.eOB.set_rightbottom(extTime, z.btm)

            // Trigger lines
            d.trigLn.set_xy1(z.left_time, z.trig)
            d.trigLn.set_xy2(time, z.trig)
            d.trigLn.set_color(color.new(css, 15))

            d.trigExt.set_xy1(time, z.trig)
            d.trigExt.set_xy2(extTime, z.trig)
            d.trigExt.set_color(color.new(css, 40))

            // Far edge
            d.farLn.set_xy1(z.left_time, far)
            d.farLn.set_xy2(time, far)
            d.farLn.set_color(color.new(css, 75))

            d.farExt.set_xy1(time, far)
            d.farExt.set_xy2(extTime, far)
            d.farExt.set_color(color.new(css, 85))

            // Original text placement
            if showLabel
                string txt = (z.bearish ? "BULL TRAP" : "BEAR TRAP") + (showMetric and z.total_vol > 0 ? "\n" + f_vol_text(z.total_vol) + " TRAPPED" : "")

                d.eOB.set_text(txt)
                d.eOB.set_text_size(size.auto)
                d.eOB.set_text_halign(text.align_right)
                d.eOB.set_text_formatting(text.format_bold)
                d.eOB.set_text_color(chart.fg_color)

f_draw_all(bullTraps, bullDraws, bearCol)
f_draw_all(bearTraps, bearDraws, bullCol)

// ┌────────────────────────────── BOSWaves ─ Retest Markers ─────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
bool bullRetest = bearTrapRetest and trend == 1
bool bearRetest = bullTrapRetest and trend == -1

plotchar(bullRetest ? low : na, "Bull Retest", "✦", location.absolute, bullCol, size=size.small)
plotchar(bearRetest ? high : na, "Bear Retest", "✦", location.absolute, bearCol, size=size.small)

// ┌────────────────────────────── BOSWaves ─ Alerts ─────────────────────────────────┐
// └──────────────────────────────────────────────────────────────────────────────────┘
alertcondition(switchUp, "Trend Long", "Liquidity Trap Structure LONG {{ticker}} {{interval}}")
alertcondition(switchDown, "Trend Short", "Liquidity Trap Structure SHORT {{ticker}} {{interval}}")
alertcondition(bullTrap, "Bull Trap", "Bull trap confirmed | {{exchange}}:{{ticker}}")
alertcondition(bearTrap, "Bear Trap", "Bear trap confirmed | {{exchange}}:{{ticker}}")
alertcondition(bullTrapRetest or bearTrapRetest, "Trap Retest", "Trap zone retest | {{exchange}}:{{ticker}}")